package images;

import game.Data;
import game.TextManager;
import static images.ImageManager.*;
import java.awt.Color;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashMap;
import math.Point;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.lwjgl.opengl.GL11;
import org.newdawn.slick.opengl.Texture;
import saves.EntidadeNave;
import saves.items.Equipamento;
import saves.items.Equipamento.LocalEquip;

/**
 *
 * @author Luan
 */
public class ShipLoader {

    public static Texture imgShips;
    public static HashMap<Integer, Modelo> modelos;
    
    public static final int DEFAULT_SHIP_SIZE = 64;
    
    public static final int[][] METRICS_SHIP = { //OFF, DEF, TRA, CUS
        {23, 18, 23, 53, 57, 35, 11, 11, 24, 6},
        {13, 38, 13, 53, 57, 35, 11, 19, 40, 6},
        {30, 38, 14, 58, 58, 30, 12, 17, 35, 6}
    };
    
    public static final int
            W_ASA = 0,
            H_ASA = 3,
            W_CORPO = 1,
            H_CORPO = 4,
            W_P_LATE = 2,
            H_P_LATE = 5,
            W_P_TRAS = 2,
            H_P_TRAS = 6,
            X_ENER = 7,
            Y_ENER = 8,
            TAM_ENER = 9;
    
    public static Equipamento[][] DEFAULT_EQUIPS;

    public static class Modelo {
        
        int[] tipos;
        /*
         * Toda peça tem pelo menos um tipo. Uma carcaça sempre tem somente um.
         * Os tipos referem-se aos diferentes templates para encaixe das peças:
         * Ofensiva, Defensiva, Comerciante e Balanceada.
         * Se um modelo tiver mais de um tipo, isso quer dizer que ele pode ser
         * usado em qualquer um dos tipos que apresentar (seus encaixes batem
         * em todos). Isso é particularmente interessante entre Trans. En.'s,
         * mas pode ser usado também em outros casos.
         */
        
        LocalEquip[] locais;
        /*
         * Regras para que um modelo se aplique a vários locais:
         * 1 - Os locais estão sempre em ordem crescente, ou seja, de maior importância para a menor.
         *     Então: Carcaça > Asa > Prop. Tras. > Prop. Lat. > Trans. En.
         * 2 - Um Prop. Lat. ou um Trans. En. somente podem estar associados a um grupo que contenha uma asa (pois eles ficam na asa).
         *     Não faz sentido haver um equip que seja uma carcaça e um Trans. En. - ele ficaria flutuando sem a asa.
         */
        
        HashMap<Integer, ImageInfo> estados;
        /*
         * Normalmente presente nos propulsores, os diferentes estados representam as várias ações que a nave possa estar fazendo,
         * como: andando para a frente, girando, freando, etc. Ainda assim, é possível que uma carcaça tenha alguns indicadores luminosos, por exemplo;
         * basta adicioná-los aqui.
         */
        
        float[][] attributes; //modifiers
        /*
         * Esses são os modificadores para os atributos básicos que esse modelo oferece, para serem combinados com o material e modificador geral da peça.
         * Se não estiverem especificados no arquivo JSON, serão gerados aleatoriamente.
         */
        
        String originalFileName;
        /*
         * Only for reference purposes, without the S.
         */
        
        float wingHeight;
        /*
         * Used only if wing. Determine the height of the collision box wing points.
         */

        public Modelo(int[] locais, int[] tipos, int p, ImageInfo m, float wh, String name, float[][] att) {
            this(toLocalEquips(locais), tipos, p, m, wh, name, att);
        }

        public LocalEquip[] getLocais() {
            return locais;
        }

        private static LocalEquip[] toLocalEquips(int[] a) {
            LocalEquip[] l = new LocalEquip[a.length];
            for (int i = 0; i < l.length; i++)
                l[i] = LocalEquip.toLocalEquip(a[i]);
            return l;
        }

        public Modelo(LocalEquip[] locais, int[] tipos, int p, ImageInfo m, float wh, String name, float[][] att) {
            this.locais = locais;
            this.tipos = tipos;
            this.estados = new HashMap<>();
            this.originalFileName = name;
            this.wingHeight = wh;
            this.setAttributes(att);
            this.add(p, m);
        }

        public void setAttributes(float[][] attributes) {
            this.attributes = new float[TextManager.t().ATTRIBUTES.length][3];
            for (int i = 0; i < this.attributes.length; i++)
                for (int j = 0; j < this.attributes[i].length; j++)
                    this.attributes[i][j] = attributes[i][j] == -2 ? (normallyHas(i, j) ? random() : 0) : attributes[i][j];
        }
        
        private boolean normallyHas(int c1, int c2) {
            for (int i = 0; i < locais.length; i++)
                if (locais[i].normallyHas(c1, c2))
                    return true;
            return false;
        }

        public static double prob(int n) {
            return 1 - .5 / Math.pow(2, n);
        }

        public static float random() { //generates a most likely random number
            float r = (float) Math.random(), n = (float) Math.random();
            if (n > .5) {
                n = 2 * n - 1; //n: [-1, 1]
                if (n < 0 && Math.random() > .75)
                    n *= -1; //it's way more likely to be positive number, of course
            }
            if (r <= prob(0))
                return 0;
            else if (r <= prob(1))
                n *= .3; //tamanho do segmento
            else if (r <= prob(2)) {
                n *= .2; //tamanho do segmento
                if (n < 0)
                    n -= .3; //inicio do segmento
                else
                    n += .3;
            } else if (r <= prob(3)) {
                n *= .3; //tamanho do segmento
                if (n < 0)
                    n -= .5; //inicio do segmento
                else
                    n += .5;
            } else if (r <= prob(4)) {
                n *= .1; //tamanho do segmento
                if (n < 0)
                    n -= .8; //inicio do segmento
                else
                    n += .8;
            } else {
                n *= .1; //tamanho do segmento
                if (n < 0)
                    n -= .85; //inicio do segmento
                else
                    n += .85;
            }
            return n;
        }

        public final void add(int p, ImageInfo m) {
            this.estados.put(p, m);
        }

        public ImageInfo getState(int state) {
            ImageInfo st = estados.get(state);
            if (st != null)
                return st;
            return estados.get(0);
        }

        public int[] getTipos() {
            return tipos;
        }

        public float getWingHeight() {
            return wingHeight;
        }
    }
    private static Program program;

    private static int[] getInts(JSONArray el, int pos, String ts) {
        Object[] array = ((JSONArray) ((JSONObject) el.get(pos)).get(ts)).toArray();
        int[] v = new int[array.length];
        for (int j = 0; j < array.length; j++)
            v[j] = Integer.parseInt(array[j].toString());
        return v;
    }
    
    private static String removeS(String t) {
        for (int i = t.length() - 1; i > 0; i--)
            if (t.charAt(i) == 'S')
                return t.substring(0, i);
        return t;
    }
    
    public static void load() throws IOException {
        imgShips = ImageManager.getTexture("ships.png");

        modelos = new HashMap<>();
        double pwx = imgShips.getWidth() / imgShips.getImageWidth(), pwy = imgShips.getHeight() / imgShips.getImageHeight();
        //read json files
        //read textures info
        JSONArray el = (JSONArray) JSONValue.parse(new String(Files.readAllBytes(Paths.get(ImageManager.getPath() + "ships.json"))));
        for (int i = 0; i < el.size(); i++) {
            int cod = getInt(el, i, "codigo");
            int[] locais = getInts(el, i, "local"), tipos = getInts(el, i, "tipo");
            ImageInfo m = getImage(el, i, pwx, pwy);
            int estado = getInt(el, i, "estado");
            float wh = getFloat(el, i, "wingHeight");
            float[][] att = getFloats2(el, i, "modifiers");
            if (att == null) {
                att = new float[TextManager.t().ATTRIBUTES.length][3];
                for (int j = 0; j < att.length; j++)
                    Arrays.fill(att[j], -2);
            }
            String name = removeS(getString(el, i, "nome"));

            Modelo mod = modelos.get(cod);
            if (mod == null)
                modelos.put(cod, new Modelo(locais, tipos, estado, m, wh, name, att));
            else
                mod.add(estado, m);
        }
        
        DEFAULT_EQUIPS = new Equipamento[][] {
            {new Equipamento(5, 0, 0, 0), new Equipamento(7, 0, 0, 0), new Equipamento(8, 0, 0, 0), new Equipamento(9, 0, 0, 0), new Equipamento(3, 0, 0, 0)},
            {new Equipamento(5, 0, 0, 0), new Equipamento(7, 0, 0, 0), new Equipamento(8, 0, 0, 0), new Equipamento(9, 0, 0, 0), new Equipamento(3, 0, 0, 0)},
            {new Equipamento(5, 0, 0, 0), new Equipamento(7, 0, 0, 0), new Equipamento(8, 0, 0, 0), new Equipamento(9, 0, 0, 0), new Equipamento(3, 0, 0, 0)}};
    }

    public static void draw(EntidadeNave n, int x, int y, int s, float ang, int state) {
        final Equipamento[] eqs = new Equipamento [LocalEquip.size()];
        for (int i = 0; i < eqs.length; i++)
            eqs[i] = n.getEquip(i);
        draw(eqs, x, y, s, ang, state);
        //desenhar potenciais upgrades sendo usados?
    }

    public static void draw(Equipamento[] eqs, int x, int y, int s, float ang, int state) {
        int cx = x + s / 2, cy = y + s / 2;
        if (eqs[0] == null)
            return; //there is nothing to be drawn
        int tipoNave = modelos.get(eqs[0].getCodModelo()).tipos[0]; //toda carcaça tem um e somente um tipo, que é o tipo da nave

        ImageInfo ot = null, equips[] = new ImageInfo[eqs.length];
        if (eqs[LocalEquip.PROP_LATERAL.p()] != null)
            ot = modelos.get(eqs[LocalEquip.PROP_LATERAL.p()].getCodModelo()).getState(LocalEquip.PROP_LATERAL.getPartState(state, 1));
        for (int i = 0; i < eqs.length; i++)
            if (eqs[i] != null)
                equips[i] = modelos.get(eqs[i].getCodModelo()).getState(LocalEquip.toLocalEquip(i).getPartState(state, 0));

        //prop laterais
        float k = (float) s / DEFAULT_SHIP_SIZE;
        GL11.glPushMatrix();
        ImageInfo.rotate(ang, cx, cy);
        Color c = null, nc;
        if (equips[LocalEquip.PROP_LATERAL.p()] != null) {
            bindProgram(c = Data.getElementoInfo(eqs[LocalEquip.PROP_LATERAL.p()].getCodRevestimento()).getMainColor(), imgShips);
            draw(equips[LocalEquip.PROP_LATERAL.p()], x, y, 0, DEFAULT_SHIP_SIZE - METRICS_SHIP[tipoNave][H_P_LATE], k, 0);
            draw(ot, x, y, DEFAULT_SHIP_SIZE - METRICS_SHIP[tipoNave][W_P_LATE], DEFAULT_SHIP_SIZE - METRICS_SHIP[tipoNave][H_P_LATE], k, 1);
        }

        //prop tras
        if (equips[LocalEquip.PROP_TRASEIRO.p()] != null) {
            nc = Data.getElementoInfo(eqs[LocalEquip.PROP_TRASEIRO.p()].getCodRevestimento()).getMainColor();
            if (nc != c) {
                c = nc;
                if (c != null) unbindProgram();
                bindProgram(c, imgShips);
            }
            draw(equips[LocalEquip.PROP_TRASEIRO.p()], x, y, METRICS_SHIP[tipoNave][W_P_TRAS], DEFAULT_SHIP_SIZE - METRICS_SHIP[tipoNave][H_P_TRAS], k, 0);
        }

        //asas
        if (equips[LocalEquip.ASA.p()] != null) {
            nc = Data.getElementoInfo(eqs[LocalEquip.ASA.p()].getCodRevestimento()).getMainColor();
            if (nc != c) {
                c = nc;
                if (c != null) unbindProgram();
                bindProgram(c, imgShips);
            }
            draw(equips[LocalEquip.ASA.p()], x, y, 0, 0, k, 0);
            draw(equips[LocalEquip.ASA.p()], x, y, DEFAULT_SHIP_SIZE - METRICS_SHIP[tipoNave][W_ASA], 0, k, 1);
        }

        //transformadores
        if (equips[LocalEquip.TRANSFORMADOR.p()] != null) {
            nc = Data.getElementoInfo(eqs[LocalEquip.TRANSFORMADOR.p()].getCodRevestimento()).getMainColor();
            if (nc != c) {
                c = nc;
                if (c != null) unbindProgram();
                bindProgram(c, imgShips);
            }
            draw(equips[LocalEquip.TRANSFORMADOR.p()], x, y, METRICS_SHIP[tipoNave][X_ENER], METRICS_SHIP[tipoNave][Y_ENER], k, 0);
            draw(equips[LocalEquip.TRANSFORMADOR.p()], x, y, DEFAULT_SHIP_SIZE - METRICS_SHIP[tipoNave][X_ENER] - METRICS_SHIP[tipoNave][TAM_ENER], METRICS_SHIP[tipoNave][Y_ENER], k, 1);
        }

        nc = Data.getElementoInfo(eqs[LocalEquip.CARCACA.p()].getCodRevestimento()).getMainColor();
        if (nc != c) {
            c = nc;
            if (c != null)
                unbindProgram();
            bindProgram(c, imgShips);
        }
        //carcaça (nunca será null)
        draw(equips[LocalEquip.CARCACA.p()], x, y, METRICS_SHIP[tipoNave][W_P_LATE], 0, k, 0);
        unbindProgram();
        GL11.glPopMatrix();
    }
    
    public static void draw(Equipamento e, int x, int y, float k, int r) {
        draw(e, x, y, 0, 0, k, r);
    }
    
    public static void draw(Equipamento e, int x, int y, int dx, int dy, float k, int r) {
        Color c = Data.getElementoInfo(e.getCodRevestimento()).getMainColor();
        bindProgram(c, imgShips);
        draw(e.getCodModelo(), x, y, dx, dy, k, r);
        unbindProgram();
    }

    public static void draw(int codigo, int x, int y, float k, int r) {
        draw(modelos.get(codigo).getState(0), x, y, 0, 0, k, r);
    }

    public static void draw(int codigo, int x, int y, int dx, int dy, float k, int r) {
        draw(modelos.get(codigo).getState(0), x, y, dx, dy, k, r);
    }

    public static void draw(ImageInfo img, int x, int y, int dx, int dy, float k, int r) {
        switch (r) {
            case 0:
                img.draw((int) (x + k * dx), (int) (y + k * dy), (int) (k * img.getWidth()), (int) (k * img.getHeight()));
                break;
            case 1:
                img.drawHMirror((int) (x + k * dx), (int) (y + k * dy), (int) (k * img.getWidth()), (int) (k * img.getHeight()));
                break;
            case 2:
                img.drawVMirror((int) (x + k * dx), (int) (y + k * dy), (int) (k * img.getWidth()), (int) (k * img.getHeight()));
                break;
            case 3:
                img.drawHVMirror((int) (x + k * dx), (int) (y + k * dy), (int) (k * img.getWidth()), (int) (k * img.getHeight()));
                break;
        }
    }

    public static void bindTexture() {
        imgShips.bind();
    }

    public static Texture getTexture() {
        return imgShips;
    }

    public static Point getPontoDisparo(EntidadeNave n, int t, float ang) {
        return Point.rotacionar(new Point(DEFAULT_SHIP_SIZE / 2, -5), 360 - ang, DEFAULT_SHIP_SIZE / 2, DEFAULT_SHIP_SIZE / 2);
    }

    public static void setupShaders(String file) {
        program = new Program();
        program.addShader(ImageManager.getPath() + file); //todo change to resources
        program.linkProgram();   // link the shaders
    }

    private static void bindProgram(Color c, Texture t) {
        if (ImageManager.isGrayBinded())
            return; //if the screen is being rendered gray, there is no need to replace the colors
        
        program.bind();
        program.defineUniform("fg"); //new color
        program.defineUniform("texture"); //the texture used

        program.setUniform("fg", (float) c.getRed() / 255, (float) c.getGreen() / 255, (float) c.getBlue() / 255);
        program.setUniform("texture", 0);//t.getTextureID());
    }

    private static void unbindProgram() {
        if (ImageManager.isGrayBinded())
            return; //if the screen is being rendered gray, there is no need to replace the colors
        
        program.unbind();
    }

    public static int getWidth(int code) {
        return modelos.get(code).estados.get(0).getWidth();
    }

    public static int getHeight(int code) {
        return modelos.get(code).estados.get(0).getHeight();
    }
}
